/*
 * Project: a2
 * Copyright (C) 2008 ralfoide gmail com,
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package com.alfray.a2.gameplay;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;

import android.util.Log;

import com.alfray.a2.engine.UIEventAdapter.IEventProcessor;
import com.alfray.a2.gameplay.ActionThread.IActionHandler;


class Gameplay implements IEventProcessor, IActionHandler {

    private static String TAG = "Gameplay";
    private static boolean DEBUG = true;

    private enum Events {
        START,
        FILL,
        CHECK,
        SELECT,
        SWAP1,
        SWAP2,
        DISABLE_SELECTION,
        ENABLE_SELECTION,
        APPLY_CHAINS,
        EXEC_CHAIN,
        EXEC_POWERUP,
        REMOVE_CHAIN1,
        REMOVE_CHAIN2
    }

    private final GameState mGameState;
    private final Random mRandom;
    private final int mSxy;

    private AnimThread mAnimThread;
    private ActionThread mActionThread;

    /** Number of identical cells before a chain gets executed. Starts with
     *  3 but may change when leveing up. */
    private int mChainThreshold;
    /** Counter used by {@link #doFill()} to go thru each cell. */
    private int mFillCounter;
    private boolean mFilled;
    /** All chains */
    private ArrayList<Chain> mChains = new ArrayList<Chain>();
    private boolean mEnableSeletion;
    private Cell mPrimarySelection;
    private boolean mModeFreeze;
    private Cell mSecondarySelection;
    private Chain mExecChain;
    private boolean mStartFill;

    public Gameplay(GameState gameState) {
        mGameState = gameState;
        mSxy = mGameState.mBoardBx * mGameState.mBoardBy;
        mChainThreshold = 3;
        mFillCounter = 0;
        mRandom = new Random();
    }

    public void setAnimThread(AnimThread animThread) {
        mAnimThread = animThread;
    }

    public void setActionThread(ActionThread actionThread) {
        mActionThread = actionThread;
    }

    /**
     * Sets screen orientation information.
     *
     * @param angle Screen orientation angle in degrees. 0=down, 90=right, etc.
     * @param direction Screen orientation direction. One of {@link #DIR_DOWN}, etc.
     */
    public void setScreenAngle(int angle, int direction) {
        int oldDir = mGameState.mScreenDir;
        mGameState.setScreenAngle(angle, direction);
        if (direction != oldDir) onScreenDirChanged();
    }

    public void start() {
        mActionThread.queueEvent(Events.START);
    }

    public void resetBoard() {
        final int sx = mGameState.mBoardBx;
        final int sy = mGameState.mBoardBy;
        Cell[] cells = mGameState.mBoard;
        for (int j = 0, k = 0; j < sy; j++) {
            for (int i = 0; i < sx; i++, k++) {
                Cell c = cells[k];
                c.mBackground.setSpriteId(-1);
                c.mBackground.setVisible(false);
                c.mSprite.setSpriteId(-1);
                c.mSprite.setVisible(false);
            }
        }

        mGameState.setSelection(-1, -1);
    }

    // --- Interface IEventProcessor ----

    public void onActivate() {
        mActionThread.queueEvent(Events.SELECT);
    }

    public void onCursorMoved(int bx, int by) {
        int currBx = mGameState.mSelectionBx;
        int currBy = mGameState.mSelectionBy;

        int sx = mGameState.mBoardBx;
        int sy = mGameState.mBoardBy;

        if (currBx != bx || currBy != by) {

            if (currBx >= 0 && currBy >= 0 && currBx < sx && currBy < sy) {
                Cell c = mGameState.getCell(currBx, currBy);
                c.mBackground.setSpriteId(-1);
                c.mBackground.setVisible(false);
            }

            mGameState.setSelection(bx, by);

            if (bx >= 0 && by >= 0 && bx < sx && by < sy) {
                Cell c = mGameState.getSelectedCell();
                c.mBackground.setSpriteId(mGameState.mSpriteBgSelected);
                c.mBackground.setVisible(true);
            }
        }
    }

    public void onCursorMovedDelta(int bx, int by) {
        if (bx == 0 && by == 0) return;

        int currBx = mGameState.mSelectionBx;
        int currBy = mGameState.mSelectionBy;

        bx += currBx;
        by += currBy;
        if (bx < 0) bx = 0;
        if (by < 0) by = 0;

        int sx = mGameState.mBoardBx;
        int sy = mGameState.mBoardBy;
        if (bx >= sx) bx = sx - 1;
        if (by >= sy) by = sy - 1;

        if (currBx != bx || currBy != by) {
            if (currBx >= 0 && currBy >= 0 && currBx < sx && currBy < sy) {
                Cell c = mGameState.getCell(currBx, currBy);
                c.mBackground.setSpriteId(-1);
                c.mBackground.setVisible(false);
            }

            mGameState.setSelection(bx, by);

            Cell c = mGameState.getSelectedCell();
            c.mBackground.setSpriteId(mGameState.mSpriteBgSelected);
            c.mBackground.setVisible(true);
        }
    }

    public long getTouchDelayMs() {
        return AnimThread.FPS_MS;
    }

    // ----- Interface IActionHandler ---

    public void onActionEvent(Object event) {
        if (DEBUG) Log.d(TAG, "Event: " + event.toString());
        switch((Events)event) {
            case START:
                doStart();
                break;
            case FILL:
                doFill();
                break;
            case CHECK:
                doCheck();
                break;
            case SELECT:
                doSelect();
                break;
            case SWAP1:
                doSwap1();
                break;
            case SWAP2:
                doSwap2();
                break;
            case DISABLE_SELECTION:
                setEnableSelection(false);
                break;
            case ENABLE_SELECTION:
                setEnableSelection(true);
                break;
            case APPLY_CHAINS:
                doApplyChains();
                break;
            case EXEC_CHAIN:
                doExecChain();
                break;
            case EXEC_POWERUP:
                doExecPowerup();
                break;
            case REMOVE_CHAIN1:
                doRemoveChain1();
                break;
            case REMOVE_CHAIN2:
                doRemoveChain2();
                break;
        }
    }

    private void setEnableSelection(boolean enableSelection) {
        mEnableSeletion = enableSelection;
    }

    // ---------------------------------------

    private void doStart() {
        setEnableSelection(true);
        mStartFill = true;
        mActionThread.queueEvent(Events.FILL);
    }

    // ---------------------------------------
    /**
     * Fill action.
     *
     * - find first empty cell (keep current counter)
     * - sprite: falls from N cells depending on current orientation
     * - set background to normal
     * - sprite already accounted in target
     * - find chain depending on neighbors
     *     - check 4 neighbors
     *     - if same sprite id, add to their chain
     *     - if other neighbors have same sprite id, convert to this chain
     *   Note: because of freeze mode, chains may be > threshold.
     * - schedule next fill or fill-done when wrapping
     */
    private void doFill() {
        Cell c = null;
        while (mFillCounter < mSxy) {
            c = mGameState.mBoard[mFillCounter];
            if (c.mSprite.mSpriteId < 0) break;
            mFillCounter++;
        }

        if (mFillCounter == mSxy) {
            // everything filled, schedule fill-done (aka check) if we
            // actually filled something.
            mStartFill = false;
            mFillCounter = 0;
            if (mFilled) {
                mFilled = false;
                mActionThread.queueEvent(Events.CHECK);
            }
            return;
        }

        mFillCounter++;
        mFilled = true;

        //-- if (DEBUG) Log.d(TAG, "Fill: " + Integer.toString(mFillCounter));

        // Set new sprite
        SpriteState st = c.mSprite;
        st.setVisible(false);
        st.setSpriteId(mGameState.mSpriteMain);
        int ci = c.mI;
        int cj = c.mJ;


        int k = mRandom.nextInt(GameState.BASE_COLORS.length);
        st.setColor(k, GameState.BASE_COLORS[k]);

        insertInChains(c);

        if (mStartFill) {
            mAnimThread.startAppears(st, AnimThread.FPS / 4 /*1/4sec*/);
        } else {
            // for orientation=0:
            int bx = mGameState.mBoardBx;
            int by = mGameState.mBoardBy;
            int xpx = 0;
            int xpy = 0;

            switch(mGameState.mScreenDir) {
            case GameState.DIR_DOWN:
                //    to.....from (by == top)
                xpy = cj - by;
                break;
            case GameState.DIR_UP:
                xpy = cj;
                break;
            case GameState.DIR_LEFT:
                //    to.....from (bx == right)
                xpx = ci - bx;
                break;
            case GameState.DIR_RIGHT:
                xpx = ci;
                break;
            }

            // make it fall at ~4 cells/second
            int dist = xpx + xpy;
            if (dist < 0) dist = -dist;

            int frames = AnimThread.FPS * dist / 4;
            if (frames <= 0) frames = 1;

            if (xpx != 0) xpx = (xpx << 16) / frames;
            if (xpy != 0) xpy = (xpy << 16) / frames;

            mAnimThread.startFlyOver(st, xpx, xpy, frames);
        }

        st.setVisible(true);

        mActionThread.queueDelayFor(50 /*ms*/);
        mActionThread.queueEvent(Events.FILL);
    }

    /**
     * Find chain by looking at the four cells around but not the corners
     * If there are not chains around to merge with, create a new one.
     */
    private void insertInChains(Cell cell) {
        cell.setChain(null);

        int colorIndex = cell.mSprite.mColorIndex;
        int ci = cell.mI;
        int cj = cell.mJ;

        if (colorIndex < 0 || cell.mSprite.mSpriteId < 0) return;

        Cell c2;

        c2 = mGameState.getCell(ci - 1, cj);
        if (c2 != null) chainMerge(c2, cell, colorIndex);

        c2 = mGameState.getCell(ci + 1, cj);
        if (c2 != null) chainMerge(c2, cell, colorIndex);

        c2 = mGameState.getCell(ci, cj - 1);
        if (c2 != null) chainMerge(c2, cell, colorIndex);

        c2 = mGameState.getCell(ci, cj + 1);
        if (c2 != null) chainMerge(c2, cell, colorIndex);

        if (cell.mChain == null) {
            // Failed to find a chain to merge with. Create a new one.
            Chain chain = new Chain();
            mChains.add(chain);
            chain.insert(cell);
            if (DEBUG) Log.d(TAG, String.format("New chain (%d): %s", mChains.size(), chain.toString()));
        }
    }

    /**
     * Merge the source cell with the chain of the given dest cell.
     * <p/>
     * Workflow:
     * - if colors are not the same, abort.
     * - if the source doesn't have a chain, append source to the destination chain.
     * - if the source has a chain and the destination is using another chain, then
     *   change all the destination chain to the source chain (i.e. merge them.)
     * <p/>
     * To insert a cell in an existing chain:
     * - set the sprite state chain to the chain
     * - append the sprite state to the chain
     */
    private void chainMerge(Cell dest, Cell source, int colorSource) {
        if (dest.mSprite == null || dest.mSprite.mColorIndex != colorSource) return;

        Chain sc = (Chain) source.mChain;
        Chain dc = (Chain) dest.mChain;

        if (dc != null) {
            if (sc == null) {
                dc.insert(source);
            } else {
                for (Object item : dc.mList.toArray()) {
                    sc.insert((Cell) item);
                }
            }
        }
    }

    // ---------------------------------------
    /**
     * Check action.
     *
     * - schedule game done if there are no possible moves.
     *   Note: a possible move happens if there's a chain with
     *     threshold-1 cells in a row (H or V), one filler and then one
     *     matching cell.
     * - schedule apply-chains
     */
    private void doCheck() {

        // TODO check game done

        mActionThread.queueEvent(Events.APPLY_CHAINS);
    }

    // ---------------------------------------
    /**
     * Select action.
     *
     * - done if selection is disabled
     * - find cell touched
     * - if primary selection is none:
     *     - make primary selection
     *     - blink slow
     *     - done
     * - if cell is primary selection:
     *     - remove primary selection
     *     - stop blink
     *     - if freeze mode:
     *          - mark chain as pending
     *          - schedule apply-chain
     *     - done
     * - if freeze mode and secondary selection in primary chain:
     *     - mark chain as pending
     *     - schedule apply-chain
     *     - done
     * - secondary selection is not valid (i.e. not next to primary selection):
     *     - remove primary selection
     *     - stop blink
     *     - done
     * - apply secondary selection: "swap"
     *     - blink both selections fast
     *     - schedule disable-selection
     *     - schedule delay
     *     - schedule swap
     */
    private void doSelect() {

        if (!mEnableSeletion) return;

        Cell c = mGameState.getSelectedCell();
        if (c == null || c.mSprite == null) return;

        // set as primary selection if there isn't any
        if (mPrimarySelection == null) {
            mPrimarySelection = c;
            c.mSprite.setAutoRotationY(2);
            return;
        }

        Chain newChain = (Chain) c.mChain;
        // remove if this is the primary selection (toggle)
        if (mPrimarySelection == c) {
            mPrimarySelection = null;
            c.mSprite.setAutoRotationY(0);

            if (DEBUG) assert newChain != null;

            if (mModeFreeze) {
                newChain.setPending(true);
                mActionThread.queueEvent(Events.APPLY_CHAINS);
            }

            return;
        }

        Chain primaryChain = (Chain) mPrimarySelection.mChain;
        // is this freeze mode and secondary selection in priamry chain?
        if (mModeFreeze && newChain == primaryChain) {
            newChain.setPending(true);
            mActionThread.queueEvent(Events.APPLY_CHAINS);
            return;
        }

        // is this a valid secondary selection for swap?
        boolean valid;

        // secondary selection valid if of NOT the same color and NOT the same
        // chain.
        valid = newChain != primaryChain &&
                c.mSprite.mColorIndex != mPrimarySelection.mSprite.mColorIndex;

        // are they close to each other?
        if (valid) {
            int di = c.mI - mPrimarySelection.mI;
            int dj = c.mJ - mPrimarySelection.mJ;
            if (di < 0) di = -di;
            if (dj < 0) dj = -dj;
            valid = (di == 0 && dj == 1) || (di == 1 && dj == 0);
        }

        if (!valid) {
            // disable primary selection if second one is invalid
            mPrimarySelection.mSprite.setAutoRotationY(0);
            mPrimarySelection = null;
            return;
        }

        // Both selections are valid. Disable selection. "Blink" them fast.
        mActionThread.queueEvent(Events.DISABLE_SELECTION);
        mPrimarySelection.mSprite.setAutoRotationY(4);
        mSecondarySelection = c;
        c.mSprite.setAutoRotationY(4);

        mActionThread.queueDelayFor(250 /*ms*/);
        mActionThread.queueEvent(Events.SWAP1);
    }

    // ---------------------------------------
    /**
     * Start of swap action.
     *
     * - performs the actual swap
     */
    private void doSwap1() {
        Cell c1 = mPrimarySelection;
        Cell c2 = mSecondarySelection;

        // we need to remove them from their current chains
        if (c1.mChain != null) ((Chain) c1.mChain).remove(c1);
        if (c2.mChain != null) ((Chain) c2.mChain).remove(c2);

        // swap the sprites in the cells
        SpriteState sp1 = c1.mSprite;
        c1.setSprite(c2.mSprite);
        c2.setSprite(sp1);

        // insert them back in chains
        insertInChains(c1);
        insertInChains(c2);

        mActionThread.queueDelayFor(250 /*ms*/);
        mActionThread.queueEvent(Events.SWAP2);
    }

    /**
     * End of swap action.
     *
     * - increment number of moves
     * - remove both selections
     *     - schedule delay
     *     - do not enable-selection yet (done by check -> apply-chains)
     *     - schedule check
     */
    private void doSwap2() {
        mGameState.incMoves(1);

        Cell c1 = mPrimarySelection;
        Cell c2 = mSecondarySelection;
        mPrimarySelection = null;
        mSecondarySelection = null;
        c1.mSprite.setAutoRotationY(0);
        c2.mSprite.setAutoRotationY(0);
        mActionThread.queueEvent(Events.CHECK);
    }

    // ---------------------------------------
    /**
     * Apply chain action.
     *
     * - schedule disable-selection
     * - find chain >= threshold OR marked as pending:
     *     - schedule execute-chain
     * - if no chain found:
     *     - schedule enable-selection
     *     - schedule fill
     */
    private void doApplyChains() {

        for(Chain chain : mChains) {
            boolean use = chain.mPending;
            if (!use && chain.mList.size() >= mChainThreshold) {
                use = checkChainThreshold(chain);
            }
            if (use) {
                mExecChain = chain;
                mActionThread.queueEvent(Events.DISABLE_SELECTION);
                mActionThread.queueEvent(Events.EXEC_CHAIN);
                return;
            }
        }

        // no chain found
        mActionThread.queueEvent(Events.ENABLE_SELECTION);
        mActionThread.queueEvent(Events.FILL);
    }

    /**
     * For a given chain, find whether it has at least one alignment that
     * is equal or larger to the chain exec threshold.
     */
    private boolean checkChainThreshold(Chain chain) {
        int t = (1 << mChainThreshold) - 1;

        for(int c : chain.mColsBits) {
            if (c != 0) {
                while ((c & 1) == 0) c = c >> 1;
                if ((c & t) == t) return true;
            }
        }

        for(int r : chain.mRowsBits) {
            if (r != 0) {
                while ((r & 1) == 0) r = r >> 1;
                if ((r & t) == t) return true;
            }
        }

        return false;
    }

    // ---------------------------------------
    /**
     * Exec chain action.
     *
     * - mark chain# as pending for removal
     * - if powerups:
     *     - move powerups to pending list
     *     - schedule exec-powerups
     * - if no powerups:
     *     - schedule chain-removal
     */
    private void doExecChain() {
        if (mExecChain != null) {
            if (DEBUG) Log.d(TAG, "Exec Chain: " + mExecChain.toString());

            // TODO deal with powerups
            mActionThread.queueEvent(Events.EXEC_POWERUP);
        }
        mActionThread.queueEvent(Events.REMOVE_CHAIN1);
    }

    // ---------------------------------------
    /**
     *
     * - find first powerup, execute
     * - if more powerup:
     *     - schedule execute-powerup (loop)
     * - else:
     *     - schedule chain-removal
     */
    private void doExecPowerup() {
        // TODO Auto-generated method stub

    }

    // ---------------------------------------
    /**
     *
     * - if no pending chain:
     *     - schedule apply-chains
     *     - done
     * --1--
     * - schedule all cells for shrink
     * - delay 1 second
     * --2--
     * - schedule remove backgrounds
     * - use current orientation now or fixed direction
     * - find all cells that need to fall (from-to)
     *     - schedule fall anim
     * - schedule apply-chains:
     */
    private void doRemoveChain1() {

        // the chain may have already been removed by a powerup
        if (mExecChain == null) {
            mActionThread.queueEvent(Events.APPLY_CHAINS);
            return;
        }

        // Apply score
        for (int k = 1, n = mExecChain.mList.size() - 1;  n >= 0; n--, k++) {
            mGameState.incScore(k);
        }

        mAnimThread.startShrink(mExecChain.mList, AnimThread.FPS/2 /*0.5s*/);
        mActionThread.queueDelayFor(500 /*1s*/);
        mActionThread.queueEvent(Events.REMOVE_CHAIN2);
    }

    private void doRemoveChain2() {
        // Remove sprites
        for (Cell cell : mExecChain.mList) {
            cell.mSprite.setSpriteId(-1);
            cell.mSprite.setVisible(false);
            // Remove background? Currently not supported.
        }

        // Remove chain
        mChains.remove(mExecChain);
        mExecChain = null;

        switch(mGameState.mScreenDir) {
        case GameState.DIR_DOWN:
            makeFallUpDown(1);
            break;
        case GameState.DIR_UP:
            makeFallUpDown(-1);
            break;
        case GameState.DIR_LEFT:
            makeFallLeftRight(1);
            break;
        case GameState.DIR_RIGHT:
            makeFallLeftRight(-1);
            break;
        }

        mActionThread.queueEvent(Events.APPLY_CHAINS);
    }

    /**
     * Make fall up or down.
     *
     * @param scanDir 1 for fall-down (scan up), -1 for fall-up (scan down)
     */
    private void makeFallUpDown(int scanDir) {
        int bx = mGameState.mBoardBx;
        int by = mGameState.mBoardBy;

        int rowMin = scanDir == 1 ? 0 : by - 1;
        int rowMax = scanDir == 1 ? by : -1;

        for (int col = 0; col < bx; col++) {
            Cell emptyCell = null;
            for (int row = rowMin; row != rowMax; row += scanDir) {
                Cell c = mGameState.getCell(col, row);
                boolean empty = (c.mSprite.mSpriteId == -1);
                if (emptyCell == null) {
                    if (empty) {
                        emptyCell = c;
                    }
                } else {
                    if (!empty) {
                        // Move cell from row to emptyRow
                        int emptyRow = emptyCell.mJ;

                        // First remove from its chain, if any
                        if (c.mChain != null) ((Chain)c.mChain).remove(c);

                        // Hide it
                        SpriteState st = c.mSprite;
                        st.setVisible(false);

                        // Swap with empty
                        c.setSprite(emptyCell.mSprite);
                        emptyCell.setSprite(st);

                        // Animate move & show it
                        int y = ((emptyRow - row) << 16) / (AnimThread.FPS / 2);
                        mAnimThread.startFlyOver(st, 0, y, AnimThread.FPS / 2);
                        st.setVisible(true);

                        // Put cell back into a chain
                        insertInChains(emptyCell);

                        // Find next empty cell
                        while (emptyRow != row) {
                            emptyRow += scanDir;
                            emptyCell = mGameState.getCell(col, emptyRow);
                            if (emptyCell.mSprite.mSpriteId == -1) break;
                        }
                    }
                }
            }
        }
    }

    /**
     * Make fall left or right.
     *
     * @param scanDir 1 for fall-left (scan right), -1 for fall-right (scan left).
     */
    private void makeFallLeftRight(int scanDir) {
        int bx = mGameState.mBoardBx;
        int by = mGameState.mBoardBy;

        int colMin = scanDir == 1 ? 0 : bx - 1;
        int colMax = scanDir == 1 ? bx : -1;

        for (int row = 0; row < by; row++) {
            Cell emptyCell = null;
            for (int col = colMin; col != colMax; col += scanDir) {
                Cell c = mGameState.getCell(col, row);
                boolean empty = (c.mSprite.mSpriteId == -1);
                if (emptyCell == null) {
                    if (empty) {
                        emptyCell = c;
                    }
                } else {
                    if (!empty) {
                        // Move cell from col to emptyCol
                        int emptyCol = emptyCell.mI;

                        // First remove from its chain, if any
                        if (c.mChain != null) ((Chain)c.mChain).remove(c);

                        // Hide it
                        SpriteState st = c.mSprite;
                        st.setVisible(false);

                        // Swap with empty
                        c.setSprite(emptyCell.mSprite);
                        emptyCell.setSprite(st);

                        // Animate move & show it
                        int x = ((emptyCol - col) << 16) / (AnimThread.FPS / 2);
                        mAnimThread.startFlyOver(st, x, 0, AnimThread.FPS / 2);
                        st.setVisible(true);

                        // Put cell back into a chain
                        insertInChains(emptyCell);

                        // Find next empty cell
                        while (emptyCol != col) {
                            emptyCol += scanDir;
                            emptyCell = mGameState.getCell(emptyCol, row);
                            if (emptyCell.mSprite.mSpriteId == -1) break;
                        }
                    }
                }
            }
        }
    }

    // ---------------------------------------
    /**
     * When screen direction has changed, update the background to reflect the
     * new direction.
     */
    private void onScreenDirChanged() {
        /* -- Not necessary anymore, this is done automatically by the
         * sprite renderer for all sprites now. Keep this here in case we
         * later want to *reverse* the automatic rotation. --
        Cell[] cells = mGameState.mBoard;
        final int xRotZ = mGameState.mScreenDir * (90 << 16);
        for (int n = cells.length - 1; n >= 0; n--) {
            SpriteState b = cells[n].mBackground;
            b.setRotationZ(xRotZ);
        }
        */
    }

    // -----------------

    private class Chain {
        /** Whether chain is marked as pending for exec/removal. */
        public boolean mPending;
        /** List of cells for this chain. */
        public HashSet<Cell> mList;
        /** Bitmap of cells used by this chain for each column. */
        public int[] mColsBits;
        /** Bitmap of cells used by this chain for each row. */
        public int[] mRowsBits;

        public Chain() {
            mList = new HashSet<Cell>();

            int bx = mGameState.mBoardBx;
            int by = mGameState.mBoardBy;
            assert bx < 32;
            assert by < 32;
            mColsBits = new int[bx];
            mRowsBits = new int[by];

            for (int i = 0; i < bx; i++) mColsBits[i] = 0;
            for (int i = 0; i < by; i++) mRowsBits[i] = 0;
        }

        /**
         * Adds {@link Cell} cell into this list.
         * If cell had a previous chain, remove it from the old chain first
         * (and delete the chain if it becomes empty.)
         * Sets cell's chain to this.
         */
        public void insert(Cell cell) {
            if (cell.mChain != null) ((Chain) cell.mChain).remove(cell);
            cell.setChain(this);
            mList.add(cell);

            int i = cell.mI;
            int j = cell.mJ;
            mColsBits[i] |= 1 << j;
            mRowsBits[j] |= 1 << i;
        }

        /**
         * Removes the {@link Cell} cell from this chain.
         * This nullifies the mChain pointer in the cell.
         * <p/>
         * Trying to remove a cell not from this chain has no effect.
         */
        public void remove(Cell cell) {
            Chain old = (Chain) cell.mChain;
            if (old != this) return;

            cell.mChain = null;
            mList.remove(cell);

            int i = cell.mI;
            int j = cell.mJ;
            mColsBits[i] &= ~(1 << j);
            mRowsBits[j] &= ~(1 << i);

            // delete chain if it becomes empty
            if (mList.isEmpty()) {
                mChains.remove(this);
                if (DEBUG) Log.d(TAG, String.format("Remove chain (%d): %s", mChains.size(), toString()));
            }
        }

        public void setPending(boolean pending) {
            mPending = pending;
        }
    }
}
